﻿// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 
// See License.txt in the project root for license information.
/*
 * Actor methods for Dictionary functionality
 * This is the backing store for CloudDictionary<TKey, TValue> and CloudStringDictionary<TValue>.
 * 
 * Subtle differences between this and other dictionary implementations:
 * Like .NET collections, this class allows values to be null.
 * Keys are strings, values are any object type.  (IE, to support a CloudStringDictionary<TValue> on client side)
 * 
 * TODO:
 * Resolve semantics around an empty dictionary.  Does it get removed from the actor state entirely, or linger around?
 */

using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace System.Threading.Actors.Helpers
{
    public static class DictionaryActorMethods
    {
        public const string MyTypeName = "DICTIONARY";

        // strings used for exception messages
        private const string noParameters = "Parameters may not be null";
        private const string invalidNumberParmFmt = "Expected {0} input parameters";
        private const string invalidMinNumberParmFmt = "Expected {0} or more input parameters";
        private const string invalidDictNameParm = "Dictionary name must be a string";
        private const string invalidKeyParm = "Dictionary key name must be a string";
        private const string dictionaryDoesntExist = "Couldn't find a dictionary named \"{0}\".";
        private const string invalidKeyType = "The type of keys stored within this dictionary must be \"{0}\".   You tried adding \"{1}\"." +
            "  Either destroy the existing dictionary and recreate it with your new type of data, or pick a new name.";
        private const string invalidValueType = "The type of the values stored within this dictionary must be \"{0}\".   You tried adding \"{1}\"." +
            "  Either destroy the existing dictionary and recreate it with your new type of data, or pick a new name.";

        // For CloudDictionary<TKey, TValue> - store the type of TKey so we can ensure clients are typesafe
        private static String GetKeyTypeName(String dictionaryName)
        {
            return dictionaryName + "$KeyType";
        }

        // Store the type of TValue so we can ensure clients are typesafe
        private static String GetContentTypeName(String dictionaryName)
        {
            return dictionaryName + "$ContentType";
        }

        private static void CheckDictionaryExists(IActorState state, string dictionaryName)
        {
            if (!ActorKeyType.ValidateKeyType(state, dictionaryName, MyTypeName, true))
            {
                throw new KeyNotFoundException(String.Format(dictionaryDoesntExist, dictionaryName));
            }
        }

        public static void LoadActorMethods(IActorState state)
        {
            Contract.Requires(state != null);

            // If the state provider already has knowledge of these methods, then don't redefine them.
            object temp;
            if (state.TryGet("TryConstruct", out temp)) return;

            state.Set("Get", (Func<IActorState, object[], object>)Get);
            state.Set("Set", (Func<IActorState, object[], object>)Set);
            state.Set("Clear", (Func<IActorState, object[], object>)Clear);
            state.Set("ContainsKey", (Func<IActorState, object[], object>)ContainsKey);
            state.Set("Count", (Func<IActorState, object[], object>)Count);
            state.Set("Enumerate", (Func<IActorState, object[], object>)Enumerate);
            state.Set("EnumKeys", (Func<IActorState, object[], object>)EnumKeys);
            state.Set("EnumValues", (Func<IActorState, object[], object>)EnumValues);
            state.Set("IsReadOnly", (Func<IActorState, object[], object>)IsReadOnly);
            state.Set("MultiGet", (Func<IActorState, object[], object>)MultiGet);
            state.Set("Remove", (Func<IActorState, object[], object>)Remove);
            state.Set("TryGetValue", (Func<IActorState, object[], object>)TryGetValue);
            state.Set("Destroy", (Func<IActorState, object[], object>)Destroy);

            state.Set("TryConstruct", (Func<IActorState, object[], object>)TryConstruct);
        }

        // If the dictionary doesn't exist, set the appropriate fields.
        // Parameters:
        //      Dictionary name
        //      Type name of TKey (for now, use a .NET Type name, but this should become a JSON schema for a type)
        //      Type name of TValue (for now, use a .NET Type name, but this should become a JSON schema for a type)
        // Returns:
        //      null
        //      InvalidOperationException if can't create
        public static Object TryConstruct(IActorState state, Object[] parameters)
        {
            Contract.Requires(state != null);

            if (parameters == null)
                throw new ArgumentNullException(noParameters);
            if (parameters.Length != 3)
                throw new ArgumentException(String.Format(invalidNumberParmFmt, 3));

            String dictionaryName = (String)parameters[0];
            if (dictionaryName == null)
                throw new ArgumentNullException("dictionaryName", invalidDictNameParm);
            String keyTypeName = (String)parameters[1];
            if (keyTypeName == null)
                throw new ArgumentNullException("typeName of TKey");
            String valueTypeName = (String)parameters[2];
            if (valueTypeName == null)
                throw new ArgumentNullException("typeName of TValue");
            String keyTypeFieldName = GetKeyTypeName(dictionaryName);
            String contentTypeFieldName = GetContentTypeName(dictionaryName);

            bool exists = ActorKeyType.ValidateKeyType(state, dictionaryName, MyTypeName, true);
            if (!exists)
            {
                SetCount(state, dictionaryName, 0);

                // Record that this is a dictionary
                ActorKeyType.SetKeyType(state, dictionaryName, MyTypeName);

                // Store the content type for the keys & values
                state.Set(keyTypeFieldName, keyTypeName);
                state.Set(contentTypeFieldName, valueTypeName);
            }
            else
            {
                // Check that the values this client will store within the dictionary match the type used by all other existing clients.
                Object existingTypeName;
                if (state.TryGet(contentTypeFieldName, out existingTypeName))
                {
                    if (!String.Equals((String)existingTypeName, valueTypeName))
                        throw new InvalidOperationException(String.Format(invalidValueType, existingTypeName, valueTypeName));
                }
                if (state.TryGet(keyTypeFieldName, out existingTypeName))
                {
                    if (!String.Equals((String)existingTypeName, keyTypeName))
                        throw new InvalidOperationException(String.Format(invalidKeyType, existingTypeName, keyTypeName));
                }
            }

            return null;
        }

        // Parameters:
        //      Dictionary name
        //      one or more pairs to add to hash.
        //         Each pair is a string name, Object value
        // Returns:
        //      Number of items added; updated items do not count.
        public static object Set(IActorState state, object[] parameters)
        {
            Contract.Requires(state != null);

            if (parameters == null) throw new ArgumentNullException(noParameters);
            if (parameters.Length < 3) throw new ArgumentException(String.Format(invalidMinNumberParmFmt, 3));

            var dictionaryName = (string)parameters[0];
            CheckDictionaryExists(state, dictionaryName);

            int countAdded = 0;

            // process pairs of parameters
            for (int i = 1; i + 1 < parameters.Length; i += 2)
            {
                string field = (string)parameters[i];
                if (field == null)
                {
                    throw new ArgumentNullException(invalidKeyParm);
                }
            }

            // validation done. Now write them all
            for (int i = 1; i + 1 < parameters.Length; i += 2)
            {
                // item name & value to hash
                String keyName = (String)parameters[i];
                bool exists = ContainsKey(state, dictionaryName, keyName);
                if (SetElement(state, dictionaryName, keyName, parameters[i + 1]))
                {
                    if (!exists)
                    {
                        countAdded++;
                    }
                }
            }
            if (countAdded > 0)
            {
                int oldCount = GetCount(state, dictionaryName);
                SetCount(state, dictionaryName, oldCount + countAdded);
            }

            return countAdded;
        }

        // Parameters:
        //      Dictionary name
        //      item name
        // Returns:
        //      List of Object values
        public static object MultiGet(IActorState state, object[] parameters)
        {
            Contract.Requires(state != null);
			
            if (parameters == null) throw new ArgumentNullException(noParameters);
            if (parameters.Length < 2) throw new ArgumentException(String.Format(invalidMinNumberParmFmt, 2));

            var dictionaryName = (string)parameters[0];
            CheckDictionaryExists(state, dictionaryName);

            List<Object> vals = new List<Object>(parameters.Length - 1);
            for (int i = 1; i < parameters.Length; i++)
            {
                string field = (string)parameters[i];
                if (field == null)
                {
                    throw new ArgumentNullException(invalidKeyParm);
                }

                vals.Add(GetElement(state, dictionaryName, field));
            }
            return vals;
        }

        // Parameters:
        //      Dictionary name
        //      item name
        // Returns:
        //      value
        public static object Get(IActorState state, object[] parameters)
        {
            Contract.Requires(state != null);

            if (parameters == null) throw new ArgumentNullException(noParameters);
            if (parameters.Length != 2) throw new ArgumentException(String.Format(invalidNumberParmFmt, 2));

            var dictionaryName = (string)parameters[0];
            CheckDictionaryExists(state, dictionaryName);

            string field = (string)parameters[1];
            if (field == null)
            {
                throw new ArgumentNullException(invalidKeyParm);
            }

            Object val = GetElement(state, dictionaryName, field);

            return val;
        }

        // Parameters:
        //      Dictionary name
        //      item name
        // Returns:
        //      Tuple<Boolean, Object> saying whether the value exists, and if so, what it was
        public static object TryGetValue(IActorState state, object[] parameters)
        {
            Contract.Requires(state != null);

            if (parameters == null) throw new ArgumentNullException(noParameters);
            if (parameters.Length != 2) throw new ArgumentException(String.Format(invalidNumberParmFmt, 2));

            var dictionaryName = (string)parameters[0];
            CheckDictionaryExists(state, dictionaryName);

            string field = (string)parameters[1];
            if (field == null)
            {
                throw new ArgumentNullException(invalidKeyParm);
            }

            Object value;
            bool exists = TryGetElement(state, dictionaryName, field, out value);
            Tuple<bool, Object> ret = Tuple.Create(exists, value);
            return ret;
        }

        // Parameters:
        //      Dictionary name
        //      item name
        // Returns:
        //      Boolean if the key exists in the dictionary
        //      error string
        public static object ContainsKey(IActorState state, object[] parameters)
        {
            Contract.Requires(state != null);

            if (parameters == null) throw new ArgumentNullException(noParameters);
            if (parameters.Length != 2) throw new ArgumentException(String.Format(invalidNumberParmFmt, 2));

            var dictionaryName = (string)parameters[0];
            CheckDictionaryExists(state, dictionaryName);

            string key = (string)parameters[1];
            if (key == null)
            {
                throw new ArgumentNullException(invalidKeyParm);
            }

            String fieldName = FieldKeyName(dictionaryName, key);
            Object value;
            return state.TryGet(fieldName, out value);
        }

        // Parameters:
        //      Dictionary name
        // Returns:
        //      integer count of set items
        public static object Count(IActorState state, object[] parameters)
        {
            Contract.Requires(state != null);

            if (parameters == null) throw new ArgumentNullException(noParameters);
            if (parameters.Length != 1) throw new ArgumentException(String.Format(invalidNumberParmFmt, 1));

            var dictionaryName = (string)parameters[0];
            CheckDictionaryExists(state, dictionaryName);

            int count = GetCount(state, dictionaryName);
            return count;
        }

        public static Object IsReadOnly(IActorState astate, Object[] parameters)
        {
            Contract.Requires(parameters != null);
            Contract.Requires(parameters.Length == 1);

            return false;
        }

        // Parameters:
        //      Dictionary name
        // Returns:
        //      List of strings of field names or empty list
        //      error string
        public static object EnumKeys(IActorState state, object[] parameters)
        {
            Contract.Requires(state != null);

            if (parameters == null) throw new ArgumentNullException(noParameters);
            if (parameters.Length != 1) throw new ArgumentException(String.Format(invalidNumberParmFmt, 1));

            var dictionaryName = (string)parameters[0];
            CheckDictionaryExists(state, dictionaryName);

            string prefix = FieldKeyName(dictionaryName, "");
            IEnumerable<string> fields = state.GetAllKeysWithPrefix(prefix);

            return fields.ToList<String>();
        }

        // Parameters:
        //      Dictionary name
        // Returns:
        //      List of KeyValuePair<String, Object>
        //      error string
        public static object Enumerate(IActorState state, object[] parameters)
        {
            Contract.Requires(state != null);

            if (parameters == null) throw new ArgumentNullException(noParameters);
            if (parameters.Length != 1) throw new ArgumentException(String.Format(invalidNumberParmFmt, 1));

            var dictionaryName = (string)parameters[0];
            CheckDictionaryExists(state, dictionaryName);

            // To return the right types on the client side instead of JObjects, the client needs to 
            // pass something like IList<KeyValuePair<String, TValue>> to the deserializer as the expected types.
            // Otherwise the TValue is JObject.  So, we must return a list of .NET KeyValuePairs here.
            List<KeyValuePair<String, Object>> pairs = new List<KeyValuePair<String, Object>>();
            string prefix = FieldKeyName(dictionaryName, "");
            IEnumerable<string> fields = state.GetAllKeysWithPrefix(prefix);
            foreach (string field in fields)
            {
                Object val = GetElement(state, dictionaryName, field);
                pairs.Add(new KeyValuePair<string,object>(field, val));
            }
            return pairs;
        }

        // Parameters:
        //      Dictionary name
        // Returns:
        //      List of objects of values or empty list
        //      error string
        public static object EnumValues(IActorState state, object[] parameters)
        {
            Contract.Requires(state != null);

            if (parameters == null) throw new ArgumentNullException(noParameters);
            if (parameters.Length != 1) throw new ArgumentException(String.Format(invalidNumberParmFmt, 1));

            var dictionaryName = (string)parameters[0];
            CheckDictionaryExists(state, dictionaryName);

            List<Object> values = new List<Object>();
            string prefix = FieldKeyName(dictionaryName, "");
            IEnumerable<string> fields = state.GetAllKeysWithPrefix(prefix);
            foreach (string field in fields)
            {
                Object val = GetElement(state, dictionaryName, field);
                values.Add(val);
            }
            return values;
        }

        // Parameters:
        //      Dictionary name
        //      Array of zero or more keys to remove
        // Returns:
        //      Number of items removed.
        public static object Remove(IActorState state, object[] parameters)
        {
            Contract.Requires(state != null);

            if (parameters == null) throw new ArgumentNullException(noParameters);
            if (parameters.Length < 1) throw new ArgumentException(String.Format(invalidNumberParmFmt, 1));

            var dictionaryName = (string)parameters[0];
            CheckDictionaryExists(state, dictionaryName);

            int numRemoved = 0;
            for (int i = 1; i < parameters.Length; i++)
            {
                string key = (string)parameters[i];
                if (key == null)
                    throw new ArgumentNullException(invalidKeyParm);

                String keyName = FieldKeyName(dictionaryName, key);
                Object junk;
                if (state.TryGet(keyName, out junk))
                {
                    state.Remove(keyName);
                    numRemoved++;
                }
            }

            if (numRemoved > 0)
            {
                int oldCount = GetCount(state, dictionaryName);
                SetCount(state, dictionaryName, oldCount - numRemoved);
            }

            return numRemoved;
        }

        // Parameters:
        //      Dictionary name
        // Returns:
        //      void
        public static Object Clear(IActorState state, Object[] parameters)
        {
            Contract.Requires(state != null);
            Contract.Requires(parameters != null);

            if (parameters.Length != 1)
                throw new ArgumentException(String.Format(invalidNumberParmFmt, 1));

            String dictionaryName = (String) parameters[0];
            CheckDictionaryExists(state, dictionaryName);

            string prefix = FieldKeyName(dictionaryName, "");
            IEnumerable<string> keys = state.GetAllKeysWithPrefix(prefix);
            foreach (string key in keys)
            {
                state.Remove(FieldKeyName(dictionaryName, key));
            }

            SetCount(state, dictionaryName, 0);
            return null;
        }

        // Removes the dictionary completely.  New clients must call TryConstruct again to use it.
        // Parameters:
        //      Dictionary name
        // Returns:
        //      void
        public static Object Destroy(IActorState state, Object[] parameters)
        {
            Contract.Requires(state != null);
            Contract.Requires(parameters != null);

            if (parameters.Length != 1)
                throw new ArgumentException(String.Format(invalidNumberParmFmt, 1));

            String dictionaryName = (String)parameters[0];
            if (!ActorKeyType.ValidateKeyType(state, dictionaryName, MyTypeName, true))
            {
                return null;
            }

            Destroy(state, dictionaryName);
            return null;
        }

        // state: IActorState
        // name: key name
        // Assumes that name existence has been verified
        // This is meant to be called by helpers to do cleanup
        private static void Destroy(IActorState state, string name)
        {
            // first make sure it is empty
            string prefix = FieldKeyName(name, "");
            IEnumerable<string> fields = state.GetAllKeysWithPrefix(prefix);
            foreach (string field in fields)
            {
                RemElement(state, name, field);
            }

            // then remove all traces
            state.Remove(CountKeyName(name));
            ActorKeyType.RemoveKeyType(state, name);
            state.Remove(GetContentTypeName(name));
            state.Remove(GetKeyTypeName(name));
        }


        //
        // Some utility methods to enforce consistent naming of keys among methods
        //
        private static string CountKeyName(string dictionaryName)
        {
            return String.Format("_{0}_count", dictionaryName);
        }

        private static string FieldKeyName(string dictionaryName, string objName)
        {
            return String.Format("_{0}_${1}", dictionaryName, objName);
        }

        // get count of elements in the dictionary
        // throw InvalidOperationException if not possible
        private static int GetCount(IActorState state, string dictionaryName)
        {
            Object existingCount;
            String countKey = CountKeyName(dictionaryName);
            if (!state.TryGet(countKey, out existingCount))
            {
                throw new InvalidOperationException("The hash has an invalid structure");
            }
            return (int)existingCount;
        }

        // set count of elements in dictionary
        private static void SetCount(IActorState state, string dictionaryName, int count)
        {
            state.Set(CountKeyName(dictionaryName), count);
        }

        // get count of elements or -1
        private static int TryGetCount(IActorState state, string dictionaryName)
        {
            Object existingCount;
            String countKey = CountKeyName(dictionaryName);
            if (!state.TryGet(countKey, out existingCount))
            {
                return -1;
            }
            return (int)existingCount;
        }

        // update entry and write to actor state
        private static bool SetElement(IActorState state,
                                        string dictionaryName,
                                        string name,
                                        Object value)
        {
            state.Set(FieldKeyName(dictionaryName, name), value);

            return true;
        }

        // remove entry and links
        private static void RemElement(IActorState state,
                                        string dictionaryName,
                                        string name)
        {
            state.Remove(FieldKeyName(dictionaryName, name));
        }


        private static bool ContainsKey(IActorState state, String dictionaryName, String keyName)
        {
            Object tempObject;
            return state.TryGet(FieldKeyName(dictionaryName, keyName), out tempObject);
        }

        // get element value or null
        private static Object GetElement(IActorState state,
                                                string dictionaryName,
                                                string name)
        {
            object tempObject;
            if (state.TryGet(FieldKeyName(dictionaryName, name), out tempObject))
            {
                if (tempObject != null)
                    return tempObject;
            }
            return null;
        }

        private static bool TryGetElement(IActorState state, String dictionaryName, String key, out Object value)
        {
            return state.TryGet(FieldKeyName(dictionaryName, key), out value);
        }
    }
}
